defmodule Neo4jSchema do
	@moduledoc """

	"""
	import SyntaxMacro

	defmacro schema(label, alias \\ nil, [do: block]) do
		quote do
			Module.put_attribute(__MODULE__, :label, unquote(label))
			Module.put_attribute(__MODULE__, :alias, unquote(alias))
			unquote(block)
		end
	end

	defmacro field(field, type, opts \\ []) do
		default = get_default_value(type, opts)
		info = MapLib.from_keyword(Keyword.delete(opts, :default))
					 |> Map.put(:type, type)
		is_unique = Keyword.get(opts, :is_unique)
		info = case Keyword.get(opts, :mfa) do
			nil ->
				info
			v ->
				{v, _} = Code.eval_quoted(v)
				Map.put(info, :mfa, v)
		end
		quote do
			Keyword.get(unquote(opts), :primary_key) != nil && begin do
				List.keyfind(@field, :primary_key, 0, true) || throw({:error, "Already Defined PrimaryKey"})
				Module.put_attribute(
					__MODULE__,
					:primary_key,
					unquote(Macro.escape({Atom.to_string(field), is_unique}))
				)
			end
			Module.put_attribute(__MODULE__, :field, unquote(Macro.escape({field, info})))
			Module.put_attribute(__MODULE__, :struct_field, unquote(Macro.escape({field, default})))
		end
	end

	defmacro json_desc(field, opts) do
		value = quote do
			MapLib.from_keyword(unquote(opts))
		end
		quote do
			Module.put_attribute(__MODULE__, :json_desc, unquote({field, value}))
		end
	end

	# [{"list", {m, f, a}},{"insert", {m, f, a}}]
	@doc """
		deepr 有四个函数, insert, list, update, delete, 目前pipelines 只支持 list
		%{"list" => {mod, func, []}}, 执行deepr_list 的时候会调用 list 里面定义的 pipeline.
		拼接sql之前, 可以对 args 参数做一些参数(条件设置等)
		def func(:before, user, args) do
		end
		查询得到结果之后,可以对结果做一些处理(把用户id替换成名称等)
		def func(:after, user, rows) do
		end
	"""
	defmacro pipelines(pipes) do
		quote do
			Module.put_attribute(__MODULE__, :pipelines, unquote(pipes))
		end
	end

	defmacro many_to_one(field, mod, opts) do
		value = quote do
			%{:type => :many_to_one, :mod => unquote(mod)}
			|> Map.merge(MapLib.from_keyword(unquote(opts)))
		end
		map = quote do: MapLib.from_keyword(unquote(opts))
		quote  do
			Module.put_attribute(__MODULE__, :field, unquote({field, value}))
			Module.put_attribute(__MODULE__, :many_to_one, unquote({field, value}))
			Module.put_attribute(__MODULE__, :mod_mapping_rel, unquote({mod, map}))
		end
	end

	defmacro foreign(field, mod, opts) do
		for_key = Keyword.get(opts, :for, nil)
		for_key == nil && raise TipError, "not set for key"
		v = Atom.to_string(for_key)
		opts = Keyword.put(opts, :for, v)
		value = Keyword.merge(opts, [mod: mod, type: :foreign])
		quote do
			Module.put_attribute(__MODULE__, :foreign, unquote({field, value}))
		end
	end

	defmacro mfa(mfa) do
		{mfa, _} = Code.eval_quoted(mfa)
		quote do
			Module.put_attribute(__MODULE__, :mfa, unquote(Macro.escape(mfa)))
		end
	end


	defmacro belong_to(field, mod, opts) do
		value = quote do
			%{:type => :belong_to, :mod => unquote(mod)}
			|> Map.merge(MapLib.from_keyword(unquote(opts)))
		end
		quote do
			Module.put_attribute(__MODULE__, :field, unquote({field, value}))
			Module.put_attribute(__MODULE__, :belong_to, unquote(value))
		end
	end

	defmacro one_to_many(field, mod, opts) do
		value = quote do
			%{:type => :one_to_many, :mod => unquote(mod)}
			|> Map.merge(MapLib.from_keyword(unquote(opts)))
		end
		quote do
			Module.put_attribute(__MODULE__, :field, unquote({field, value}))
			Module.put_attribute(__MODULE__, :one_to_many, unquote({field, value}))
		end
	end

	defmacro one_to_one(field, mod, opts) do
		value = quote do
			%{:type => :one_to_one, :mod => unquote(mod)}
			|> Map.merge(MapLib.from_keyword(unquote(opts)))
		end
		quote do
			Module.put_attribute(__MODULE__, :field, unquote({field, value}))
			Module.put_attribute(__MODULE__, :one_to_one, unquote({field, value}))
		end
	end

	defmacro rel_attr(field, opts \\ []) do
		value = quote do
			MapLib.from_keyword(unquote(opts))
		end
		quote do
			Module.put_attribute(__MODULE__, :rel_attr, unquote({field, value}))
		end
	end

	def get_default_value(:string, opts) do
		Keyword.get(opts, :default, nil)
	end
	def get_default_value(:json, opts) do
		Keyword.get(opts, :default, "{}")
	end
	def get_default_value(:jsonb, opts) do
		Keyword.get(opts, :default, "{}")
	end
	def get_default_value(:boolean, opts) do
		Keyword.get(opts, :default, false)
	end
	def get_default_value(:integer, opts) do
		Keyword.get(opts, :default, 0)
	end
	def get_default_value(:int8, opts) do
		Keyword.get(opts, :default, 0)
	end
	def get_default_value(:bigserial, opts) do
		Keyword.get(opts, :default, 0)
	end
	def get_default_value(:float, opts) do
		Keyword.get(opts, :default, 0)
	end
	def get_default_value(:money, opts) do
		Keyword.get(opts, :default, 0)
	end
	def get_default_value(:date, opts) do
		Keyword.get(opts, :default, nil)
	end
	def get_default_value(:ymd, opts) do
		Keyword.get(opts, :default, nil)
	end
	def get_default_value({:array, _}, _opts) do
		[]
	end
	def get_default_value(_, opts) do
		Keyword.get(opts, :default, nil)
	end


	defmacro __before_compile__(_env) do
		quote unquote: false, location: :keep do
			json_fields = @json_desc
										|> Neo4jSchema.from_keyword()
			field_attrs = @rel_attr
										|> MapLib.list_to_map()
			{_fields, fields, proto_fields} =
				@field
				|> Enum.reduce(
						 {[], %{}, []},
						 fn ({fild_name, filed_info}, {acc1, acc2, acc3}) ->
							 name = Atom.to_string(fild_name)
							 case Map.get(field_attrs, fild_name) do
								 nil ->
									 case Map.get(json_fields, fild_name) do
										 nil ->
											 {
												 [{fild_name, filed_info} | acc1],
												 Map.put(acc2, name, filed_info),
												 [{name, filed_info} | acc3]
											 }
										 json_desc ->
											 {
												 [{fild_name, filed_info} | acc1],
												 Map.put(acc2, name, Map.put(filed_info, :json_desc, json_desc)),
												 [{name, Map.put(filed_info, :json_desc, json_desc)} | acc3]
											 }
									 end
								 value ->
									 rel_name = Map.get(value, :name)
									 rel_name == nil && throw({:error, "Miss RelationName"})
									 rel_attrs = Map.get(value, :attr)
									 rel_attrs == nil && throw({:error, "Miss RelationAttrs"})
									 value =
										 Enum.map(
											 rel_attrs,
											 fn ({attr_name, attr_values}) ->
												 {
													 Atom.to_string(attr_name),
													 attr_values
												 }
											 end
										 )
										 |> Neo4jSchema.from_keyword()
										 |> Map.put(:relation_object, name)
									 info = Map.put(filed_info, rel_name, value)
									 {
										 [{fild_name, info} | acc1],
										 # 关系放在 所属的域里,这样才好查找
										 Map.put(acc2, name, info)
										 |> Map.put(rel_name, value),
										 [{rel_name, value}, {name, info} | acc3]
									 }
							 end
						 end
					 )
			foreign = @foreign
			{fields, proto_fields, foreign} = Enum.reduce(
				foreign,
				{fields, proto_fields, %{}},
				fn ({key, value}, {acc, acc2, acc3}) ->
					key = Atom.to_string(key)
					value = Neo4jSchema.from_keyword(value)
					acc3 = Map.put(acc3, key, value[:for])
					{Map.put(acc, key, value), [{key, value} | acc2], acc3}
				end
			)
			struct = @struct_field
							 |> Enum.reverse()
							 |> MapLib.list_to_map()
							 |> Map.put(:__struct__, __MODULE__)
			belong_to = @belong_to
									|> case do
											 [] ->
												 %{}
											 h ->
												 hd(h)
										 end
									|> Map.delete(:type)

			one_to_many = @one_to_many
										|> Enum.reverse()
										|> MapLib.list_to_map()
			many_to_one = @many_to_one
										|> Enum.reverse()
										|> MapLib.list_to_map()
			label = @label
			alias = @alias

			mapping = @mod_mapping_rel
								|> MapLib.list_to_map()

			def primary_key() do
				unquote(Macro.escape(@primary_key))
			end

			def struct() do
				unquote(Macro.escape(struct))
			end

			def belong_to() do
				unquote(Macro.escape(belong_to))
			end

			def one_to_many() do
				unquote(Macro.escape(one_to_many))
			end

			def many_to_one() do
				unquote(Macro.escape(many_to_one))
			end


			def foreign() do
				unquote(Macro.escape(foreign))
			end

			def label() do
				unquote(label)
			end

			def alias() do
				unquote(alias)
			end

			def fields() do
				unquote(Macro.escape(fields))
			end

			def mod_mapping() do
				unquote(Macro.escape(mapping))
			end

			def db_type() do
				Pango.DbType.cypher()
			end

			def deepr_object() do
				label()
				|> Macro.underscore()
			end

			def proto_fields() do
				unquote(Macro.escape(proto_fields))
			end

			relations = @one_to_many ++ @one_to_one
									|> Enum.map(&(Atom.to_string(elem(&1, 0))))
			def relations() do
				unquote(Macro.escape(relations))
			end

			def pipelines() do
				unquote(Macro.escape(from_keyword(@pipelines)))
			end



			vis = if Map.get(fields, "visible") != nil do
				%{"visible" => true}
			else
				%{}
			end
			init = if Map.get(fields, "imgs") != nil do
				%{"imgs" => "{}"}
				|> Map.merge(vis)
			else
				%{}
			end
			def new(args) do
				MapLib.set_struct(args, __MODULE__)
				|> Map.merge(unquote(Macro.escape(init)))
			end

			def mfa() do
				@mfa
			end

			defoverridable new: 1
		end
	end



	defmacro __using__(_options) do
		quote do
			import unquote(__MODULE__)
			Module.register_attribute(__MODULE__, :foreign, accumulate: true)
			Module.register_attribute(__MODULE__, :field, accumulate: true)
			Module.register_attribute(__MODULE__, :struct_field, accumulate: true)
			Module.register_attribute(__MODULE__, :belong_to, accumulate: true)
			Module.register_attribute(__MODULE__, :one_to_many, accumulate: true)
			Module.register_attribute(__MODULE__, :one_to_one, accumulate: true)
			Module.register_attribute(__MODULE__, :many_to_one, accumulate: true)
			Module.register_attribute(__MODULE__, :rel_attr, accumulate: true)
			Module.register_attribute(__MODULE__, :json_desc, accumulate: true)
			Module.register_attribute(__MODULE__, :mod_mapping_rel, accumulate: true)
			Module.put_attribute(__MODULE__, :pipelines, [])
			Module.put_attribute(__MODULE__, :mfa, nil)
			@before_compile unquote(__MODULE__)
		end
	end

	def from_keyword(keyword) do
		for {key, value} <- keyword,
				into: %{} do
			if !is_map(value) do
				{key, value}
			else
				{key, MapLib.from_keyword(value)}
			end
		end
	end

end
